package org.apache.maven.lifecycle.internal;

/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 *
 *  http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied.  See the License for the
 * specific language governing permissions and limitations
 * under the License.
 */

import org.apache.maven.artifact.Artifact;
import org.apache.maven.artifact.ArtifactUtils;
import org.apache.maven.execution.BuildSuccess;
import org.apache.maven.execution.ExecutionEvent;
import org.apache.maven.execution.MavenExecutionRequest;
import org.apache.maven.execution.MavenSession;
import org.apache.maven.lifecycle.LifecycleExecutionException;
import org.apache.maven.lifecycle.MavenExecutionPlan;
import org.apache.maven.lifecycle.Schedule;
import org.apache.maven.project.MavenProject;
import org.codehaus.plexus.component.annotations.Component;
import org.codehaus.plexus.component.annotations.Requirement;
import org.codehaus.plexus.logging.Logger;

import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.Set;
import java.util.concurrent.Callable;
import java.util.concurrent.CompletionService;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorCompletionService;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Future;

/**
 * Builds the full lifecycle in weave-mode (phase by phase as opposed to project-by-project)
 * <p/>
 * NOTE: Weave mode is still experimental. It may be either promoted to first class citizen at some
 * later point in time, and it may also be removed entirely. Weave mode has much more aggressive
 * concurrency behaviour than regular threaded mode, and as such is still under test wrt cross
 * platform stability.
 * <p/>
 * To remove weave mode from m3, the following should be removed: ExecutionPlanItem.schedule
 * w/setters and getters DefaultLifeCycles.getScheduling() and all its use ReactorArtifactRepository
 * has a reference to isWeave too. This class and its usage
 * 
 * @since 3.0
 * @author Kristian Rosenvold Builds one or more lifecycles for a full module
 *         <p/>
 *         NOTE: This class is not part of any public api and can be changed or deleted without
 *         prior notice.
 */
@Component(role = LifecycleWeaveBuilder.class)
public class LifecycleWeaveBuilder {

  @Requirement
  private MojoExecutor mojoExecutor;

  @Requirement
  private BuilderCommon builderCommon;

  @Requirement
  private Logger logger;

  @Requirement
  private ExecutionEventCatapult eventCatapult;

  private Map<MavenProject, MavenExecutionPlan> executionPlans = new HashMap<MavenProject, MavenExecutionPlan>();

  public LifecycleWeaveBuilder() {
  }

  public LifecycleWeaveBuilder(MojoExecutor mojoExecutor, BuilderCommon builderCommon, Logger logger, ExecutionEventCatapult eventCatapult) {
    this.mojoExecutor = mojoExecutor;
    this.builderCommon = builderCommon;
    this.logger = logger;
    this.eventCatapult = eventCatapult;
  }

  public void build(ProjectBuildList projectBuilds, ReactorContext buildContext, List<TaskSegment> taskSegments,
      MavenSession session, ExecutorService executor, ReactorBuildStatus reactorBuildStatus)
      throws ExecutionException, InterruptedException {
    ConcurrentBuildLogger concurrentBuildLogger = new ConcurrentBuildLogger();
    CompletionService<ProjectSegment> service = new ExecutorCompletionService<ProjectSegment>(executor);

    try {
      for (MavenProject mavenProject : session.getProjects()) {
        Artifact mainArtifact = mavenProject.getArtifact();
        if (mainArtifact != null && !(mainArtifact instanceof ThreadLockedArtifact)) {
          ThreadLockedArtifact threadLockedArtifact = new ThreadLockedArtifact(mainArtifact);
          mavenProject.setArtifact(threadLockedArtifact);
        }
      }

      final List<Future<ProjectSegment>> futures = new ArrayList<Future<ProjectSegment>>();
      final Map<ProjectSegment, Future<MavenExecutionPlan>> plans = new HashMap<ProjectSegment, Future<MavenExecutionPlan>>();

      for (TaskSegment taskSegment : taskSegments) {
        ProjectBuildList segmentChunks = projectBuilds.getByTaskSegment(taskSegment);
        Set<Artifact> projectArtifacts = new HashSet<Artifact>();
        for (ProjectSegment segmentChunk : segmentChunks) {
          Artifact artifact = segmentChunk.getProject().getArtifact();
          if (artifact != null) {
            projectArtifacts.add(artifact);
          }
        }
        for (ProjectSegment projectBuild : segmentChunks) {
          plans.put(projectBuild, executor.submit(createEPFuture(projectBuild, projectArtifacts)));
        }

        for (ProjectSegment projectSegment : plans.keySet()) {
          executionPlans.put(projectSegment.getProject(), plans.get(projectSegment).get());

        }
        for (ProjectSegment projectBuild : segmentChunks) {
          try {
            final MavenExecutionPlan executionPlan = plans.get(projectBuild).get();

            DependencyContext dependencyContext = mojoExecutor.newDependencyContext(session,
                executionPlan.getMojoExecutions());

            final Callable<ProjectSegment> projectBuilder = createCallableForBuildingOneFullModule(buildContext,
                session, reactorBuildStatus, executionPlan, projectBuild, dependencyContext, concurrentBuildLogger);

            futures.add(service.submit(projectBuilder));
          } catch (Exception e) {
            throw new ExecutionException(e);
          }
        }

        for (Future<ProjectSegment> buildFuture : futures) {
          buildFuture.get();  // At this point, this build *is* finished.
          // Do not leak threads past here or evil gremlins will get you!
        }
        futures.clear();
      }
    } finally {
      projectBuilds.closeAll();
    }
    logger.info(concurrentBuildLogger.toString());
  }

  private Callable<MavenExecutionPlan> createEPFuture(final ProjectSegment projectSegment,
      final Set<Artifact> projectArtifacts) {
    return new Callable<MavenExecutionPlan>() {
      public MavenExecutionPlan call() throws Exception {
        return builderCommon.resolveBuildPlan(projectSegment.getSession(), projectSegment.getProject(),
            projectSegment.getTaskSegment(), projectArtifacts);
      }
    };
  }

  private Callable<ProjectSegment> createCallableForBuildingOneFullModule(final ReactorContext reactorContext,
      final MavenSession rootSession, final ReactorBuildStatus reactorBuildStatus,
      final MavenExecutionPlan executionPlan, final ProjectSegment projectBuild,
      final DependencyContext dependencyContext, final ConcurrentBuildLogger concurrentBuildLogger) {
    return new Callable<ProjectSegment>() {
      public ProjectSegment call() throws Exception {
        Iterator<ExecutionPlanItem> planItems = executionPlan.iterator();
        ExecutionPlanItem current = planItems.hasNext() ? planItems.next() : null;
        ThreadLockedArtifact threadLockedArtifact = (ThreadLockedArtifact) projectBuild.getProject().getArtifact();
        if (threadLockedArtifact != null) {
          threadLockedArtifact.attachToThread();
        }
        long buildStartTime = System.currentTimeMillis();

        // muxer.associateThreadWithProjectSegment( projectBuild );

        if (reactorBuildStatus.isHaltedOrBlacklisted(projectBuild.getProject())) {
          eventCatapult.fire(ExecutionEvent.Type.ProjectSkipped, projectBuild.getSession(), null);
          return null;
        }

        eventCatapult.fire(ExecutionEvent.Type.ProjectStarted, projectBuild.getSession(), null);

        Collection<ArtifactLink> dependencyLinks = getUpstreamReactorDependencies(projectBuild);

        try {
          PhaseRecorder phaseRecorder = new PhaseRecorder(projectBuild.getProject());
          long totalMojoTime = 0;
          long mojoStart;
          while (current != null && !reactorBuildStatus.isHaltedOrBlacklisted(projectBuild.getProject())) {

            BuildLogItem builtLogItem = concurrentBuildLogger.createBuildLogItem(projectBuild.getProject(), current);
            final Schedule schedule = current.getSchedule();

            mojoStart = System.currentTimeMillis();
            buildExecutionPlanItem(current, phaseRecorder, schedule, reactorContext, projectBuild, dependencyContext);
            totalMojoTime += (System.currentTimeMillis() - mojoStart);

            current.setComplete();
            builtLogItem.setComplete();

            ExecutionPlanItem nextPlanItem = planItems.hasNext() ? planItems.next() : null;
            if (nextPlanItem != null && phaseRecorder.isDifferentPhase(nextPlanItem.getMojoExecution())) {

              final Schedule scheduleOfNext = nextPlanItem.getSchedule();
              if (scheduleOfNext == null || !scheduleOfNext.isParallel()) {
                waitForAppropriateUpstreamExecutionsToFinish(builtLogItem, nextPlanItem, projectBuild, scheduleOfNext);
              }

              for (ArtifactLink dependencyLink : dependencyLinks) {
                dependencyLink.resolveFromUpstream();
              }
            }
            current = nextPlanItem;
          }

          final BuildSuccess summary = new BuildSuccess(projectBuild.getProject(), totalMojoTime); // -
                                                                                                   // waitingTime
          reactorContext.getResult().addBuildSummary(summary);
          eventCatapult.fire(ExecutionEvent.Type.ProjectSucceeded, projectBuild.getSession(), null);
        } catch (Exception e) {
          builderCommon.handleBuildError(reactorContext, rootSession, projectBuild.getSession(),
              projectBuild.getProject(), e, buildStartTime);
        } finally {
          if (current != null) {
            executionPlan.forceAllComplete();
          }
          // muxer.setThisModuleComplete( projectBuild );
        }
        return null;
      }

    };
  }

  private void waitForAppropriateUpstreamExecutionsToFinish(BuildLogItem builtLogItem, ExecutionPlanItem nextPlanItem,
      ProjectSegment projectBuild, Schedule scheduleOfNext) throws InterruptedException {
    for (MavenProject upstreamProject : projectBuild.getImmediateUpstreamProjects()) {
      final MavenExecutionPlan upstreamPlan = executionPlans.get(upstreamProject);
      final String nextPhase = scheduleOfNext != null && scheduleOfNext.hasUpstreamPhaseDefined()
          ? scheduleOfNext.getUpstreamPhase() : nextPlanItem.getLifecyclePhase();
      final ExecutionPlanItem upstream = upstreamPlan.findLastInPhase(nextPhase);

      if (upstream != null) {
        long startWait = System.currentTimeMillis();
        upstream.waitUntilDone();
        builtLogItem.addWait(upstreamProject, upstream, startWait);
      } else if (!upstreamPlan.containsPhase(nextPhase)) {
        // Still a bit of a kludge; if we cannot connect in a sensible way to
        // the upstream build plan we just revert to waiting for it all to
        // complete. Real problem is per-mojo phase->lifecycle mapping
        builtLogItem.addDependency(upstreamProject, "No phase tracking possible ");
        upstreamPlan.waitUntilAllDone();
      } else {
        builtLogItem.addDependency(upstreamProject, "No schedule");
      }
    }
  }

  private Collection<ArtifactLink> getUpstreamReactorDependencies(ProjectSegment projectBuild) {
    Collection<ArtifactLink> result = new ArrayList<ArtifactLink>();
    for (MavenProject upstreamProject : projectBuild.getTransitiveUpstreamProjects()) {
      Artifact upStreamArtifact = upstreamProject.getArtifact();
      if (upStreamArtifact != null) {
        Artifact dependencyArtifact = findDependency(projectBuild.getProject(), upStreamArtifact);
        if (dependencyArtifact != null) {
          result.add(new ArtifactLink(dependencyArtifact, upStreamArtifact));
        }
      }

      Artifact upStreamTestScopedArtifact = findTestScopedArtifact(upstreamProject);
      if (upStreamTestScopedArtifact != null) {
        Artifact dependencyArtifact = findDependency(projectBuild.getProject(), upStreamArtifact);
        if (dependencyArtifact != null) {
          result.add(new ArtifactLink(dependencyArtifact, upStreamTestScopedArtifact));
        }
      }
    }
    return result;
  }

  private Artifact findTestScopedArtifact(MavenProject upstreamProject) {
    if (upstreamProject == null) {
      return null;
    }

    List<Artifact> artifactList = upstreamProject.getAttachedArtifacts();
    for (Artifact artifact : artifactList) {
      if (Artifact.SCOPE_TEST.equals(artifact.getScope())) {
        return artifact;
      }
    }
    return null;
  }

  private static boolean isThreadLockedAndEmpty(Artifact artifact) {
    return artifact instanceof ThreadLockedArtifact && !((ThreadLockedArtifact) artifact).hasReal();
  }

  private static Artifact findDependency(MavenProject project, Artifact upStreamArtifact) {
    if (upStreamArtifact == null || isThreadLockedAndEmpty(upStreamArtifact)) {
      return null;
    }

    String key = ArtifactUtils.key(upStreamArtifact.getGroupId(), upStreamArtifact.getArtifactId(),
        upStreamArtifact.getVersion());
    final Set<Artifact> deps = project.getDependencyArtifacts();
    for (Artifact dep : deps) {
      String depKey = ArtifactUtils.key(dep.getGroupId(), dep.getArtifactId(), dep.getVersion());
      if (key.equals(depKey)) {
        return dep;
      }
    }
    return null;

  }

  private void buildExecutionPlanItem(ExecutionPlanItem current, PhaseRecorder phaseRecorder, Schedule schedule,
      ReactorContext reactorContext, ProjectSegment projectBuild, DependencyContext dependencyContext)
      throws LifecycleExecutionException {
    if (schedule != null && schedule.isMojoSynchronized()) {
      synchronized (current.getPlugin()) {
        buildExecutionPlanItem(reactorContext, current, projectBuild, dependencyContext, phaseRecorder);
      }
    } else {
      buildExecutionPlanItem(reactorContext, current, projectBuild, dependencyContext, phaseRecorder);
    }
  }

  private void buildExecutionPlanItem(ReactorContext reactorContext, ExecutionPlanItem node,
      ProjectSegment projectBuild, DependencyContext dependencyContext, PhaseRecorder phaseRecorder)
      throws LifecycleExecutionException {

    MavenProject currentProject = projectBuild.getProject();

    long buildStartTime = System.currentTimeMillis();

    CurrentPhaseForThread.setPhase(node.getLifecyclePhase());

    MavenSession sessionForThisModule = projectBuild.getSession();
    try {

      if (reactorContext.getReactorBuildStatus().isHaltedOrBlacklisted(currentProject)) {
        return;
      }

      BuilderCommon.attachToThread(currentProject);

      mojoExecutor.execute(sessionForThisModule, node.getMojoExecution(), reactorContext.getProjectIndex(),
          dependencyContext, phaseRecorder);

      final BuildSuccess summary = new BuildSuccess(currentProject, System.currentTimeMillis() - buildStartTime);
      reactorContext.getResult().addBuildSummary(summary);
    } finally {
      Thread.currentThread().setContextClassLoader(reactorContext.getOriginalContextClassLoader());
    }
  }

  public static boolean isWeaveMode(MavenExecutionRequest request) {
    return "true".equals(request.getUserProperties().getProperty("maven3.weaveMode"));
  }

  public static void setWeaveMode(Properties properties) {
    properties.setProperty("maven3.weaveMode", "true");
  }

  static class ArtifactLink {
    private final Artifact artifactInThis;

    private final Artifact upstream;

    ArtifactLink(Artifact artifactInThis, Artifact upstream) {
      this.artifactInThis = artifactInThis;
      this.upstream = upstream;
    }

    public void resolveFromUpstream() {
      artifactInThis.setFile(upstream.getFile());
      artifactInThis.setRepository(upstream.getRepository());
      artifactInThis.setResolved(true); // Or maybe upstream.isResolved()....
    }
  }
}
